import math
from typing import Optional

from basis import *


class Line:
    interpolate_split_threshold = 0.5

    def __init__(self, is_relative: bool, x1: float, y1: float, x2: float, y2: float, debug: Optional[str] = None):
        self.x1 = x1
        self.y1 = y1
        self.x2 = x2 if not is_relative else x1 + x2
        self.y2 = y2 if not is_relative else y1 + y2
        self.min_x = min(self.x1, self.x2)
        self.min_y = min(self.y1, self.y2)
        self.max_x = max(self.x1, self.x2)
        self.max_y = max(self.y1, self.y2)
        self.debug = debug

    def last_point(self) -> tuple[float, float]:
        return self.x2, self.y2

    def intersects(self, other: 'Line') -> bool:
        if self.x1 == self.x2 and self.y1 == self.y2 or other.x1 == other.x2 and other.y1 == other.y2:
            return False
        if self.min_x > other.max_x or self.max_x < other.min_x or self.min_y > other.max_y or self.max_y < other.min_y:
            return False
        if isinstance(other, Line):
            return self.intersects_line(other)
        raise NotImplementedError()

    # simplification, do not optimize loops to remove intersections, just check if there are any
    def intersects_line(self, other: 'Line') -> bool:
        # https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect
        r = self.x2 - self.x1, self.y2 - self.y1
        s = other.x2 - other.x1, other.y2 - other.y1
        rxs = cross_product(r, s)
        q_p = other.x1 - self.x1, other.y1 - self.y1
        q_pxr = cross_product(q_p, r)
        if rxs == 0:
            rr = dot_product(r, r)
            if q_pxr == 0:
                o1_on_self = dot_product(q_p, r) / rr
                sr = dot_product(r, s)
                if sr > 0:
                    o2_on_self = o1_on_self + sr / rr
                else:
                    o2_on_self = o1_on_self
                    o1_on_self = o2_on_self + sr / rr
                # if o2_on_self > o1_on_self:
                #     if o2_on_self < 0 or o2_on_self > 1:
                #         return False
                #     elif o1_on_self < 0:
                #         if o2_on_self > 1:
                #             return (self.x1, self.x2), fraction(self, )
                return o1_on_self < 1 and o2_on_self > 0  # strict inequality here is only when detecting intersections
            else:
                return False
        else:
            q_pxs = cross_product(q_p, s)
            t = q_pxs / rxs
            u = q_pxr / rxs
            return 0 < t < 1 and 0 < u < 1  # strict inequality here is only when detecting intersections, not fixing

    @staticmethod
    def interpolate_cubic(is_relative: bool, x_start: float, y_start: float, x1: float, y1: float, x2: float, y2: float,
                          x_end: float, y_end: float) -> list['Line']:
        res = []  # type: list['Line']
        if is_relative:
            x1 += x_start
            y1 += y_start
            x2 += x_start
            y2 += y_start
            x_end += x_start
            y_end += y_start
        max_depth = 10
        params = (x_start, y_start, x1, y1, x2, y2, x_end, y_end)
        res.extend(Line._interpolate_cubic_step(params, 0, x_start, y_start, 1, x_end, y_end, max_depth))
        return res

    @staticmethod
    def _interpolate_cubic_step(params: tuple[float,...], t1: float, x1: float, y1: float,
                                t2: float, x2: float, y2: float,
                                depth: int) -> list['Line']:
        if depth == 0:
            return [Line(False, x1, y1, x2, y2)]
        else:
            t = (t1 + t2) / 2
            a = (1 - t) * (1 - t) * (1 - t)
            b = 3 * t * (1 - t) * (1 - t)
            c = 3 * t * t * (1 - t)
            d = t * t * t
            x_mid = a * params[0] + b * params[2] + c * params[4] + d * params[6]
            y_mid = a * params[1] + b * params[3] + c * params[5] + d * params[7]
            if x_mid < -1 or x_mid > 1700 or y_mid < -1 or y_mid > 2800:
                raise ValueError(f'interpolated point is out of bounds: {x_mid}, {y_mid} for t {t} {params}')
            xu = x1 - x_mid
            yu = y1 - y_mid
            xv = x2 - x_mid
            yv = y2 - y_mid
            u_length = xu * xu + yu * yu
            v_length = xv * xv + yv * yv
            if u_length > Line.interpolate_split_threshold or v_length > Line.interpolate_split_threshold:
                sin_a2 = cross_product((xu, yu), (xv, yv)) ** 2 / (u_length * v_length)
                if abs(sin_a2) > 0.001:
                    return (Line._interpolate_cubic_step(params, t1, x1, y1, t, x_mid, y_mid, depth - 1) +
                            Line._interpolate_cubic_step(params, t, x_mid, y_mid, t2, x2, y2, depth - 1))
            return [Line(False, x1, y1, x_mid, y_mid), Line(False, x_mid, y_mid, x2, y2)]

    @staticmethod
    def interpolate_quad(is_relative: bool, x_start: float, y_start: float, x1: float, y1: float,
                          x: float, y: float) -> list['Line']:
        return [Line(is_relative, x_start, y_start, x, y)]

    @staticmethod
    def interpolate_arc(is_relative: bool, x_start: float, y_start: float, rx: float, ry: float,
                        x_axis_rotation: float, large_arc_flag: bool, sweep_flag: bool, x: float, y: float):
        raise NotImplementedError()

    def leftmost_x(self, y: float) -> float:
        if self.y1 == y:
            return self.x1 if self.y2 != y else self.min_x
        elif self.y2 == y:
            return self.x2
        elif self.min_y < y < self.max_y:
            return (y - self.y1) * (self.x2 - self.x1) / (self.y2 - self.y1) + self.x1
        else:
            return math.inf

    # for given y coordinate returns minimum and maximum x coordinates and direction
    # if line only touches y coordinates but not intersects returns half of direction value
    def line_spec(self, y: float) -> tuple[float, float, float]:
        if self.y1 == y and self.y2 == y:
            return self.min_x, self.max_x, 0
        direction = 1 if self.y2 > self.y1 else -1
        if self.y1 == y:
            return self.x1, self.x1, direction / 2
        elif self.y2 == y:
            return self.x2, self.x2, direction / 2
        elif self.min_y < y < self.max_y:
            x = (y - self.y1) * (self.x2 - self.x1) / (self.y2 - self.y1) + self.x1
            return x, x, direction
        else:
            raise ValueError(f'invalid y coordinate: {y} for line {self.to_d()}')

    def adjacent_x(self, other: 'Line') -> bool:
        assert self.y1 == self.y2 and other.y1 == other.y2, (self, other)
        return (self.min_x <= other.max_x <= self.max_x or self.min_x <= other.min_x <= self.max_x or
                other.min_x <= self.min_x <= other.max_x)

    def flip(self):
        return Line(False, self.x2, self.y2, self.x1, self.y1)

    def __repr__(self):
        return f'({self.x1:0.2f}, {self.y1:0.2f}, {self.x2:0.2f}, {self.y2:0.2f}, {self.to_d()}{", "+self.debug if self.debug else ""})'

    def to_d(self):
        return f'L{self.x2:.3f},{self.y2:.3f}'
